﻿using System;
using System.Collections.Generic;
using System.Linq;

using DanielVaughan.AI.NeuralNetworking;
using DanielVaughan.Tests.AI.NeuralNetworking;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DanielVaughan.Tests
{
	/// <summary>
    ///This is a test class for NeuralNetworkTest and is intended
    ///to contain all NeuralNetworkTest Unit Tests
    ///</summary>
	[TestClass]
	public class NeuralNetworkTest
	{
		const double high = .99;
		const double middle = .5;
		const double low = .01;

		/// <summary>
		///Gets or sets the test context which provides
		///information about and functionality for the current test run.
		///</summary>
		public TestContext TestContext { get; set; }

		#region Additional test attributes
		// 
		//You can use the following additional attributes as you write your tests:
		//
		//Use ClassInitialize to run code before running the first test in the class
		//[ClassInitialize()]
		//public static void MyClassInitialize(TestContext testContext)
		//{
		//}
		//
		//Use ClassCleanup to run code after all tests in a class have run
		//[ClassCleanup()]
		//public static void MyClassCleanup()
		//{
		//}
		//
		//Use TestInitialize to run code before running each test
		//[TestInitialize()]
		//public void MyTestInitialize()
		//{
		//}
		//
		//Use TestCleanup to run code after each test has run
		//[TestCleanup()]
		//public void MyTestCleanup()
		//{
		//}
		//
		#endregion


		/// <summary>
		///A test for Train
		///</summary>
		[TestMethod]
		public void TrainShouldBuildNetworkForXor()
		{
			double lowLow, lowHigh, highLow, highHigh;

			double[][] input = new [] { new[] { high, high }, new[] { low, high }, new[] { high, low }, new[] { low, low }};
			double[][] expectedOutput = new [] {new[] {low}, new[] {high}, new[] {high}, new[] {low}};

			var trainingSet = new TrainingSet(new List<LayerStimulus>
			                                  	{
			                                  		new LayerStimulus(new[] { high, high }), 
													new LayerStimulus(new[] { low, high }), 
													new LayerStimulus(new[] { high, low }), 
													new LayerStimulus(new[] { low, low })
			                                  	},
												new List<LayerStimulus> { 
													new LayerStimulus(new[] { low }), 
													new LayerStimulus(new[] { high }), 
													new LayerStimulus(new[] { high }), 
													new LayerStimulus(new[] { low }) 
												});

			var target = new NeuralNetwork(2, 2, 1);

			int iterations = 50;
			int count = 0;

			do
			{
				count++;

				target.Train(trainingSet, iterations);

				target.SetInputValue(0, false);
				target.SetInputValue(1, false);

				target.Pulse();

				lowLow = target.GetOutputValue(0);

				target.SetInputValue(0, true);
				target.SetInputValue(1, false);

				target.Pulse();

				highLow = target.GetOutputValue(0);

				target.SetInputValue(0, false);
				target.SetInputValue(1, true);

				target.Pulse();

				lowHigh = target.GetOutputValue(0);

				target.SetInputValue(0, true);
				target.SetInputValue(1, true);

				target.Pulse();

				highHigh = target.GetOutputValue(0);

			} while (count < 90001 && 
				(highHigh > (middle + low) / 2
				|| lowHigh < (middle + high) / 2
				|| highLow < (middle + low) / 2
				|| lowLow > (middle + high) / 2));

			Assert.IsTrue(count < 90000, "Number of iterations exceeded expected value.");
		}

		readonly bool[][] xorBoolInput = new[] { new[] { true, true }, new[] { false, true }, new[] { true, false }, new[] { false, false } };
		readonly bool[][] xorBoolOutput = new[] { new[] { false }, new[] { true }, new[] { true }, new[] { false } };

		/// <summary>
		///A test for Train
		///</summary>
		[TestMethod]
		public void TrainShouldBuildNetworkForXorBool()
		{
			bool falseFalse, falseTrue, trueFalse, trueTrue;

			var input = xorBoolInput;
			var expectedOutput = xorBoolOutput;

			var target = new NeuralNetwork(2, 2, 1) { LearningRate = 3 };

			int iterations = 5;
			target.Train(input, expectedOutput, iterations);
			int count = 0;

			do
			{
				count++;

				target.Train(input, expectedOutput, iterations);

				target.InputAsBooleans = new[] {false, false};
				target.Pulse();
				falseFalse = target[0];

				target.InputAsBooleans = new[] { true, false };
				target.Pulse();
				trueFalse = target[0];

				target.InputAsBooleans = new[] { false, true };
				target.Pulse();
				falseTrue = target[0];

				target.InputAsBooleans = new[] { true, true };
				target.Pulse();
				trueTrue = target[0];

			} while (count < 90001 &&
				(trueTrue
				|| !falseTrue
				|| !trueFalse
				|| falseFalse));

			Assert.IsTrue(count < 90000, "Number of iterations exceeded expected value.");
		}

		/// <summary>
		///A test for MeasureAccuracy
		///</summary>
		[TestMethod]
		public void MeasureAccuracyShouldReturnALowResultBeforeTraining()
		{
			var input = xorBoolInput;
			var expectedOutput = xorBoolOutput;

			var target = new NeuralNetwork(2, 2, 1) { LearningRate = 3 };

			double accuracy = target.MeasureAccuracy(input, expectedOutput);
			/* Accuracy should be around 50% because we haven't trained it yet. */
			double expectedAccuracy = .6;
			Assert.IsTrue(accuracy < expectedAccuracy, "Expected accuracy to be less than "
				+ expectedAccuracy + " but it was " + accuracy);
		}

		/// <summary>
		///A test for MeasureAccuracy
		///</summary>
		[TestMethod]
		public void MeasureAccuracyShouldReturnAHighResultAfterTraining()
		{
			var input = xorBoolInput;
			var expectedOutput = xorBoolOutput;

			var target = new NeuralNetwork(2, 2, 1);
			int iterations = 5000;
			target.Train(input, expectedOutput, iterations);
			double accuracy = target.MeasureAccuracy();
			double expectedAccuracy = .5;
			Assert.IsTrue(accuracy > expectedAccuracy, "Expected accuracy to be greater than "
				+ expectedAccuracy + " but it was " + accuracy);

			target.Train(input, expectedOutput, iterations);
			accuracy = target.MeasureAccuracy();
			expectedAccuracy = .7;
			Assert.IsTrue(accuracy > expectedAccuracy, "Expected accuracy to be greater than "
				+ expectedAccuracy + " but it was " + accuracy);

			target.Train(input, expectedOutput, iterations);
			accuracy = target.MeasureAccuracy();
			expectedAccuracy = .7;
			Assert.IsTrue(accuracy > expectedAccuracy, "Expected accuracy to be greater than "
				+ expectedAccuracy + " but it was " + accuracy);
		}

		[TestMethod]
		public void TrainShouldTimeoutIfAccuracyNotReached()
		{
			var input = xorBoolInput;
			var expectedOutput = xorBoolOutput;

			var target = new NeuralNetwork(2, 2, 1);
			var timedTrainingResult = target.Train(input, expectedOutput, 1, 1);
			Assert.AreEqual(TrainingResult.RanOutOfTime, timedTrainingResult.TrainingResult);
			Assert.IsTrue(timedTrainingResult.AccuracyAttained < 1, "Accuracy obtained should be less than 1.");

			target = new NeuralNetwork(2, 2, 1);
			timedTrainingResult = target.Train(input, expectedOutput, 1, 1000);
			Assert.AreEqual(TrainingResult.RanOutOfTime, timedTrainingResult.TrainingResult);
			Assert.IsTrue(timedTrainingResult.AccuracyAttained < 1, "Accuracy obtained should be less than 1.");
		}

		[TestMethod]
		public void TrainShouldNotTimeoutIfAccuracyReached()
		{
			var input = xorBoolInput;
			var expectedOutput = xorBoolOutput;

			var target = new NeuralNetwork(2, 2, 1);
			double soughtAccuracy = .01;
			var timedTrainingResult = target.Train(input, expectedOutput, soughtAccuracy, 100);
			string message = string.Format("Accuracy obtained should be greater than {0}, but was only {1}.",
										   soughtAccuracy, timedTrainingResult.AccuracyAttained);
			Assert.AreEqual(TrainingResult.Success, timedTrainingResult.TrainingResult, message);
			Assert.IsTrue(timedTrainingResult.AccuracyAttained > soughtAccuracy, message);

			target = new NeuralNetwork(2, 2, 1);
			soughtAccuracy = .7;
			timedTrainingResult = target.Train(input, expectedOutput, soughtAccuracy, 1000);
			message = string.Format("Accuracy obtained should be greater than {0}, but was only {1}.",
			                               soughtAccuracy, timedTrainingResult.AccuracyAttained);
			Assert.AreEqual(TrainingResult.Success, timedTrainingResult.TrainingResult, message);
			Assert.IsTrue(timedTrainingResult.AccuracyAttained > soughtAccuracy, message);

			soughtAccuracy = .72;
			timedTrainingResult = target.Train(input, expectedOutput, soughtAccuracy, 10000);
			message = string.Format("Accuracy obtained should be greater than {0}, but was only {1}.",
										   soughtAccuracy, timedTrainingResult.AccuracyAttained);
			Assert.AreEqual(TrainingResult.Success, timedTrainingResult.TrainingResult, message);
			Assert.IsTrue(timedTrainingResult.AccuracyAttained > soughtAccuracy, message);
		}

		[TestMethod]
		public void TrainShouldCorrectlyTrainForOneOutput()
		{
			var inputNeuronCount = 5;
			var outputNeuronCount = 50;
			var hiddenNeuronCount = inputNeuronCount;
            
			var input = new double[inputNeuronCount];
			for (int i = 0; i < input.Length; i++)
			{
				input[i] = low;
			}

			var output = new double[outputNeuronCount];
			for (int i = 0; i < output.Length; i++)
			{
				output[i] = low;
			}

			var neuralNetwork = new NeuralNetwork(inputNeuronCount, hiddenNeuronCount, outputNeuronCount);
			neuralNetwork.Input = input;
			neuralNetwork.Pulse();
			var toto = neuralNetwork.Output;

			output[0] = high;
			var trainingSet = new TrainingSet(new List<LayerStimulus> { new LayerStimulus(input) },
											  new List<LayerStimulus> { new LayerStimulus(output) });
			neuralNetwork.Train(trainingSet, 500);

			neuralNetwork.Input = input;
			neuralNetwork.Pulse();
			toto = neuralNetwork.Output;
			var indexOfHighest = NeuralNetworkTimeSeries.GetIndexOfHighest(toto);
			Assert.AreEqual(0, indexOfHighest);
		}

		[TestMethod]
		public void TrainShouldLearnCorrectlyOnRepeatTraining()
		{
			var neuralNetwork = new NeuralNetwork(4, 4, 4);
			int trainingIterations = 1000;
			var input = new[] {low, high, low, high};
			var output = new [] {low, low, low, high};

			var trainingSet = new TrainingSet(new List<LayerStimulus> {new LayerStimulus(input)},
			                                  new List<LayerStimulus> {new LayerStimulus(output)});
			neuralNetwork.Train(trainingSet, trainingIterations);
			neuralNetwork.Input = new [] {low, high, low, high};
			neuralNetwork.Pulse();
			var actualOutput = neuralNetwork.Output.ToList();
			var indexOfHighest = NeuralNetworkTimeSeries.GetIndexOfHighest(actualOutput);
			Assert.AreEqual(3, indexOfHighest);

			input = new[] { low, high, high, high };
			output = new[] { low, low, high, low };
			trainingSet = new TrainingSet(new List<LayerStimulus> { new LayerStimulus(input) },
											  new List<LayerStimulus> { new LayerStimulus(output) });
			neuralNetwork.Train(trainingSet, trainingIterations);
			neuralNetwork.Input = input;
			neuralNetwork.Pulse();
			actualOutput = neuralNetwork.Output.ToList();
			indexOfHighest = NeuralNetworkTimeSeries.GetIndexOfHighest(actualOutput);
			Assert.AreEqual(2, indexOfHighest);

			input = new[] { low, high, low, high };
			output = new[] { low, low, low, high };
			trainingSet = new TrainingSet(new List<LayerStimulus> { new LayerStimulus(input) },
											  new List<LayerStimulus> { new LayerStimulus(output) });
			neuralNetwork.Train(trainingSet, trainingIterations);
			neuralNetwork.Input = new[] { low, high, low, high };
			neuralNetwork.Pulse();
			actualOutput = neuralNetwork.Output.ToList();
			indexOfHighest = NeuralNetworkTimeSeries.GetIndexOfHighest(actualOutput);
			Assert.AreEqual(3, indexOfHighest);

			input = new[] { low, high, high, high };
			output = new[] { low, low, high, low };
			trainingSet = new TrainingSet(new List<LayerStimulus> { new LayerStimulus(input) },
											  new List<LayerStimulus> { new LayerStimulus(output) });

			neuralNetwork.Train(trainingSet, trainingIterations);
			neuralNetwork.Input = input;
			neuralNetwork.Pulse();
			actualOutput = neuralNetwork.Output.ToList();
			indexOfHighest = NeuralNetworkTimeSeries.GetIndexOfHighest(actualOutput);
			Assert.AreEqual(2, indexOfHighest);
            
			neuralNetwork.Input = new[] { low, high, low, high };
			neuralNetwork.Pulse();
			actualOutput = neuralNetwork.Output.ToList();
			indexOfHighest = NeuralNetworkTimeSeries.GetIndexOfHighest(actualOutput);
			Assert.AreEqual(3, indexOfHighest, string.Format(
				"Output: {0}, {1}, {2}, {3}", actualOutput[0], actualOutput[1], actualOutput[2], actualOutput[3]));
		}
	}
}
